<?php
/**
 ***********************************************************************************************************************
 * Thor(雷神) MySQL同步Redis Hashes 组件
 ***********************************************************************************************************************
 */

namespace artisan\thor;

/**
 * Class thor
 * @package artisan\thor
 */
class thor
{
    /**
     * 缓存前缀
     */
    private $prefix = 'Thor:';

    /**
     * 缓存有效期
     */
    private $expire = 864000;

    /**
     * 触发同步数据到缓存的时间
     * now      -> 立即同步
     * shutdown -> 请求结束前
     * finish   -> 请求结束后
     *
     * @var string
     */
    private $trigger = 'finish';

    /**
     * 数据处理模型
     * @var \artisan\thor\model
     */
    private $model;

    /**
     * @var array
     */
    private $closures = [];

    /**
     * 数据库部分操作采用注入依赖模式
     * @param \artisan\thor\model $model
     * @param int $expire
     * @param string $trigger
     */
    public function __construct(\artisan\thor\model $model, $expire = 864000, $trigger = 'finish')
    {
        $this->model = $model;
        $this->expire = abs(intval($expire));
        $this->trigger = (string) $trigger;
    }

    /**
     * @param \artisan\thor\model $model
     */
    public function setModel(\artisan\thor\model $model)
    {
        $this->model = $model;
    }

    /**
     * @return \artisan\thor\model
     */
    public function getModel()
    {
        return $this->model;
    }

    /**
     * @param $expire
     */
    public function setExpire($expire)
    {
        $this->expire = intval($expire);
    }

    /**
     * @return int
     */
    public function getExpire()
    {
        return $this->expire;
    }


    /**
     * @param string $trigger
     */
    public function setTrigger($trigger = 'shutdown')
    {
        $this->trigger = $trigger;
    }


    /**
     * @return string
     */
    public function getTrigger()
    {
        return $this->trigger;
    }

    /**
     * 添加数据
     * @param array $dict
     * @return int
     */
    public function add(array $dict)
    {
        if(empty($dict)) {
            return 0;
        }

        // 写入数据库
        $ident_val = $this->getModel()->add($dict);

        // 同步到缓存
        if($ident_val) {

            // 注册一个回调函数
            $this->sync(function() use($ident_val) {
                // 重新拉取一遍该条数据所有字段
                $dict = $this->getModel()->get([$this->getModel()->ident() => $ident_val]);
                try {
                    // 写入缓存
                    $this->getCache()->hMset($this->getKey($ident_val), $dict);
                    $this->getCache()->expire($this->getKey($ident_val), $this->getExpire());

                    // 重建其他唯一字段索引 和 主键自增ID的映射关系
                    $uniques = $this->getModel()->unique();
                    if(!empty($uniques)) {
                        foreach($uniques as $key => $val) {
                            if(!empty($dict[$val])) {
                                $this->setRelation($val, $dict[$val], $ident_val);
                            }
                        }
                    }
                } catch(\RedisException $e) {
                    logWarning('Redis hGetAll fail! ['.$e->getMessage().']', 'thor::get', 'redis');
                }

            });

        }

        return $ident_val;
    }

    /**
     * 从 Thor 获取数据 如果缓存中拉取不到 拉取数据库 并且重新缓存到Thor
     * @param $ident_val
     * @param string $ident_name
     * @return array
     */
    public function get($ident_val, $ident_name = '')
    {
        if(empty($ident_val)) {
            return [];
        }

        $dict = [];

        // 如果使用的不是主键，而是其他唯一字段，获取第一主键ID的值
        if(!empty($ident_name) && $ident_name != $this->getModel()->ident()) {

            // 转换 获取主键值
            $ident_val0 = $this->getIdentVal($ident_name, $ident_val);

            // 失败 取数据库
            if(empty($ident_val0)) {
                // 如果$ident_name是数据表真实的唯一索引字段 则查数据库
                if(in_array($ident_name, $this->getModel()->unique())) {
                    $dict = $this->getModel()->get([$ident_name => $ident_val]);
                    // 重新缓存数据
                    if(!empty($dict)) {
                        $ident_val = $dict[$this->getModel()->ident()];
                        $this->sync(function() use($ident_val) {
                            $this->rewrite($ident_val);
                        });
                    }
                    return $dict;
                }
                // 否则直接返回失败
                return [];
            }

            // 成功 替换主键值
            $ident_val = $ident_val0;
            $ident_name = $this->getModel()->ident();
        }

        // 优先查找数据
        try {
            $dict = $this->getCache()->hGetAll($this->getKey($ident_val));
        } catch(\RedisException $e) {
            logWarning('Redis hGetAll fail! ['.$e->getMessage().']', 'thor::get', 'redis');
        }


        // 找不到 取数据库
        if(empty($dict)) {
            $dict = $this->getModel()->get([!$ident_name ? $this->getModel()->ident() : $ident_name => $ident_val]);
            // 重新缓存数据
            if(!empty($dict)) {
                $ident_val = $dict[$this->getModel()->ident()];
                $this->sync(function() use($ident_val) {
                    $this->rewrite($ident_val);
                });
            }
        }
        return $dict;
    }


    /**
     * 按照多个ID或其他唯一键值获取数据
     * @param array $ident_vals
     * @param string $ident_name
     * @return array
     */
    public function mGet(array $ident_vals, $ident_name = '')
    {
        if(empty($ident_vals)) {
            return array();
        }
        $result = [];
        foreach($ident_vals as $ident_val) {
            $result[] = $this->get($ident_val, $ident_name);
        }
        return $result;
    }

    /**
     * @param $ident_val
     * @param $field
     * @param string $ident_name
     * @return array|null|string
     */
    public function getVal($ident_val, $field, $ident_name = '')
    {
        if(empty($ident_val) || empty($field)) {
            return array();
        }

        $val = null;

        // 如果使用的不是主键，而是其他唯一字段，获取第一主键ID的值
        if(!empty($ident_name) && $ident_name != $this->getModel()->ident()) {

            // 转换 获取主键值
            $ident_val0 = $this->getIdentVal($ident_name, $ident_val);

            // 失败 取数据库
            if(empty($ident_val0)) {
                return $this->getModel()->get([$ident_name => $ident_val], $field);
            }

            // 成功 替换主键值
            $ident_val = $ident_val0;
            $ident_name = $this->getModel()->ident();
        }

        // 优先查找数据
        try {
            $val = $this->getCache()->hGet($this->getKey($ident_val), $field);
        } catch(\RedisException $e) {
            logWarning('Redis hGetAll fail! ['.$e->getMessage().']', 'thor::get', 'redis');
        }

        // 找不到 取数据库
        if($val === null) {
            return $this->getModel()->get([!$ident_name ? $this->getModel()->ident() : $ident_name => $ident_val], $field);
        }
        return $val;

    }

    /**
     * @param $ident_vals
     * @param $field
     * @param string $ident_name
     * @return array
     */
    public function mGetVal($ident_vals, $field, $ident_name = '')
    {
        if(empty($ident_vals)) {
            return array();
        }
        $result = [];
        foreach($ident_vals as $ident_val) {
            $result[$ident_val] = $this->getVal($ident_val, $field, $ident_name);
        }
        return $result;
    }

    /**
     * 更新数据
     * @param $ident_val
     * @param array $dict
     * @return bool
     */
    public function set($ident_val, array $dict)
    {
        if(empty($ident_val) || empty($dict)) {
            return false;
        }
        $ret = $this->getModel()->set([$this->getModel()->ident() => $ident_val], $dict);
        if($ret) {

            // 注册一个回调函数
            $this->sync(function() use($ident_val, $dict) {
                // 判断缓存是否存在
                $exist = false;
                try {
                    $exist = $this->getCache()->exists($this->getKey($ident_val));
                } catch(\RedisException $e) {
                    logWarning('Redis exists fail! ['.$e->getMessage().']', 'thor::set', 'redis');
                }

                $dict0 = [];
                // 如果缓存不存在 或者过期
                if(empty($exist)) {
                    // 重新拉取一遍该条数据所有字段
                    $dict0 = $this->getModel()->get([$this->getModel()->ident() => $ident_val]);
                }

                // 合并结果集
                $dict = array_merge($dict, $dict0);

                try {
                    $this->getCache()->hMset($this->getKey($ident_val), $dict);
                    $this->getCache()->expire($this->getKey($ident_val), $this->getExpire());
                } catch(\RedisException $e) {
                    logWarning('Redis hMset fail! ['.$e->getMessage().']', 'thor::set', 'redis');
                }

                // 重建其他唯一字段索引 和 主键自增ID的映射关系
                $uniques = $this->getModel()->unique();
                if(!empty($uniques)) {
                    foreach($uniques as $key => $val) {
                        if(!empty($dict[$val])) {
                            $this->setRelation($val, $dict[$val], $ident_val);
                        }
                    }
                }

            });

        }

        return $ret;
    }

    /**
     * 删除数据 同步执行
     * 注:安全及信息及时性考虑,会优先删除缓存中的数据,不管数据库中的数据有没有真正删除成功
     * @param $ident_val
     * @return bool
     */
    public function del($ident_val)
    {
        if(empty($ident_val)) {
            return false;
        }

        // 重新获取一遍该条记录
        $dict = $this->getModel()->get([$this->getModel()->ident() => $ident_val]);

        // ① 删除映射关系
        $uniques = $this->getModel()->unique();
        if(!empty($uniques)) {
            foreach($uniques as $unique) {
                if(!empty($dict[$unique])) {
                    try {
                        $this->getCache()->hDel($this->getRelationKey($unique), $dict[$unique]);
                    } catch(\RedisException $e) {
                        logWarning('Redis hDel fail! ['.$e->getMessage().']', 'thor::del', 'redis');
                    }
                }
            }
        }

        // ② 删除主键缓存hash
        try {
            $this->getCache()->del($this->getKey($ident_val));
        } catch(\RedisException $e) {
            logWarning('Redis del fail! ['.$e->getMessage().']', 'thor::del', 'redis');
        }

        // ③ 从数据库中删除
        return $this->getModel()->del([$this->getModel()->ident() => $ident_val]);
    }


    /**
     * 设置更多关系
     *
     * 解释：以用户表为例
     *
     * 获取一条用户数据使用唯一主键UID，thor->get(UID)，但很多时候，你想通过其他字段来缓存里取信息怎么办
     *
     * 比如，你想用该用户登录SESSIONID来获取缓存中的用户信息。
     *
     * 现在你可以这样设置，首先调用此方法，给UID和SESSIONID创建一个映射关系。relation()
     *
     * 然后你就可以通过SESSIONID来获取用户信息了，thor->get(SESSIONID_VAL, SESSIONID)
     * @param $ident_another_name
     * @param $ident_another_val
     * @param $ident_val
     * @param string $expire
     * @return bool|int
     */
    public function setRelation($ident_another_name, $ident_another_val, $ident_val, $expire = '')
    {
        if(count(array_filter([$ident_another_name, $ident_another_val, $ident_val])) != 3) {
            return false;
        }

        try {
            $ret = $this->getCache()->hSet($this->getRelationKey($ident_another_name), $ident_another_val, $ident_val);
            $expire = $expire ? intval($expire) : $this->getExpire();
            $this->getCache()->expire($this->getRelationKey($ident_another_name), $expire);
            return $ret;
        } catch(\RedisException $e) {
            logWarning('Redis hSet fail! ['.$e->getMessage().']', 'thor::relation', 'redis');
        }

        return false;
    }

    /**
     * @param $ident_another_name
     * @param $ident_another_val
     * @return null|string
     */
    public function getIdentVal($ident_another_name, $ident_another_val)
    {
        if(count(array_filter([$ident_another_name, $ident_another_val])) != 2) {
            return null;
        }

        try {
            return $this->getCache()->hGet($this->getRelationKey($ident_another_name), $ident_another_val);
        } catch(\RedisException $e) {
            logWarning('Redis hGet fail! ['.$e->getMessage().']', 'thor::getIdentVal', 'redis');
        }

        return null;
    }


    /**
     * 重写缓存 计划任务中可以使用
     * @param $ident_vals
     * @return array
     */
    public function rewrite($ident_vals)
    {
        if(empty($ident_vals)) {
            return array();
        }
        $ident_vals = is_array($ident_vals) ? $ident_vals : [$ident_vals];
        $reports = [];
        foreach($ident_vals as $ident_val) {
            // 记录重写状态
            $report[$ident_val] = ['status' => 'fail'];
            // 重新从数据库拉取信息
            $dict = $this->getModel()->get([$this->getModel()->ident() => $ident_val]);
            if($dict) {
                try {
                    // ① 重写缓存
                    $ret = $this->getCache()->hMset($this->getKey($ident_val), $dict);
                    $this->getCache()->expire($this->getKey($ident_val), $this->getExpire());

                    // ② 重写其他唯一字段映射
                    $uniques = $this->getModel()->unique();
                    if(!empty($uniques)) {
                        foreach($uniques as $unique) {
                            if(!empty($dict[$unique])) {
                                try {
                                    $this->getCache()->hSet($this->getRelationKey($unique), $dict[$unique], $ident_val);
                                    $this->getCache()->expire($this->getRelationKey($unique), $this->getExpire());

                                } catch(\RedisException $e) {
                                    logWarning('Redis hSet fail! ['.$e->getMessage().']', 'thor::rewrite', 'redis');
                                }
                            }
                        }
                    }

                    // 标记重写成功
                    if($ret) {
                        $report[$ident_val] = ['status' => 'success'];
                    }

                } catch(\RedisException $e) {
                    logWarning('Redis hMset fail! ['.$e->getMessage().']', 'thor::rewrite', 'redis');
                }
            }
            $reports[] = $report;
        }
        return $reports;
    }

    /**
     * @return \Redis
     */
    private function getCache()
    {
        return \artisan\cache::getInstance();
    }

    /**
     * @return string
     */
    private function getKey($ident_val)
    {
        return $this->prefix . $this->getModel()->table() . ':ident:' . $ident_val;
    }

    /**
     * @param $ident_another_name
     * @return string
     */
    private function getRelationKey($ident_another_name)
    {
        return $this->prefix . $this->getModel()->table() . ':relation:' . $ident_another_name;
    }

    /**
     * 执行同步
     * @param \Closure $closure
     */
    private function sync(\Closure $closure)
    {
        if(!empty($closure)) {
            switch($this->trigger) {
                // 在请求结束前执行同步
                case 'shutdown':
                    register_shutdown_function($closure);
                    break;
                // 在请求结束后执行同步
                case 'finish':
                    later($closure);
                    break;
                // 立即执行同步
                case 'now':
                    call_user_func($closure);
                    break;
            }
        }
    }

}